#!/usr/bin/python
# -*- coding: utf8 -*-
"""
    My own module handling mobiles. 
    This module is using gammu and its python bindings and AT Commands (via emulated serial port)
"""

import gammu
import serial
import os
from time import sleep

class MobileDevice:
    __ConnectionState = ""               # connection state
    __Log = ["default"]
   
##    Default class MobileDevice methods
   
    def __init__(self, gammuCS = -1, serialP = ""):
        """
        Initializes device.
        
        If you don't want to use gammu then set gammuCS parameter to -1 or leave default.
        If you don't want to use serial port, set serialP to empty string or leave default.
        In each variant, if you don't change default value of parameter, variable used to describe that connection will be empty (None)
        """
        if gammuCS >= 0:
            self.__GammuConfigSection = gammuCS
#            if self.gammuConnect():            # commented because both gammu and serial cannot be used at one time
#                self.__ConnectionState += "Connection via gammu (config section %i) established!\n" % self.__GammuConfigSection
#            else:
#                print "Connection via gammu failed!"
        else:
            self.__GammuConfigSection = None
            
        if serialP != "":
            if os.name == 'posix':
                self.__SerialPort = serialP
            if os.name == 'nt':
                self.__SerialPort = int(serialP)-1       # -1 because pySerial under Windows starts counting ports from 0 (which is COM1)
            if self.serialConnect():
                self.__initialize()
                self.__ConnectionState += "Connection with serial port %s established!\n" % self.__SerialPort
            else:
                print "Connection via Serial Port failed!"
        else:
            self.__SerialPort = None
        
    def __str__(self):
        return self.__ConnectionState

    def __repr__(self):
        return self.__str__()
    
#    def __del__(self):                        # not necessary now
#        if self.__State != None:
#            self.gammuDisconnect()

##    Connection methods
    
    def gammuConnect(self):
        """
        Sets up gammu connection.
        """
        if not self.isGammuSetUp():
            return False
        elif not self.isGammuStateCreated():
            self.__State = gammu.StateMachine()
            if self.__GammuConfigSection == 0:
                self.__State.ReadConfig()
            else:
                self.__State.ReadConfig(self.__GammuConfigSection, 0)
            self.__State.Init()
            # print self.__State.GetConfig() # prints various information about config
            return True
        else:
            return False
    
    def gammuDisconnect(self):
        """
        Disconnects with gammu by deleting self.__State (which is gammu.StateMachine() variable)
        """
        if not self.isGammuSetUp():
            return False
        elif self.isGammuStateCreated():
            del self.__State
            self.__State = None
            return True
        else:
            return False
    
    def isGammuSetUp(self):
        """
        Checks if gammu was set up
        """
        if self.__GammuConfigSection == None:
            print "Gammu wasn't set up!"
            return False
        else:
            return True
        
    def isGammuStateCreated(self):
        """
        Checks if gammu StateMachine was created
        """
        try:
            if self.__State != None:
                return True
        except:
            print "Gammu StateMachine wasn't created yet!"
            return False
        else:
            return False
    
    def serialConnect(self):
        """
        Sets up connection via serial port
        """
        if not self.isSerialSetUp():
            self.__Port = None
            return False
        elif not self.isPortCreated():
            self.__Port = serial.Serial(self.__SerialPort, timeout = 1)
            return True
        else:
            return False
        
    def serialDisconnect(self):
        """
        Closes previously opened port
        """
        if not self.isSerialSetUp():
            return False
        try:
            self.__Port.close()
            del self.__Port
            self.__Port = None
            return True
        except:
            return False
            
    def isSerialSetUp(self):
        """
        Checks if serial port was set up
        """
        if self.__SerialPort == None:
            print "Serial Port wasn't set up!"
            return False
        else:
            return True
        
    def isPortCreated(self):
        """
        Checks if port was created
        """
        try:
            if self.__Port != None:
                return True
        except:
            print "Serial Port wasn't created yet!"
            return False
        else:
            return False


##    General AT handling methods

    def __sendATCommand(self, atCommand):
        """
        Sends a single AT command to previously opened port
        
        Few Syntax Rules:
            - A command string should start with "AT" or "at", except for the commands "A/" and "+++". At or aT are invalid.
            - Several commands can be given in one command string.
            - The commands can be given in upper or lower case.
            - A command string should contain less than 40 characters.
            - When an error is made during the typing of the command, it can be corrected using the backspace key.
            - Commands and command strings must be terminated with an <ENTER>, except +++ and A/
            - A telephone number can exist of following characters:
                1 2 3 4 5 6 7 8 9 * = , ; # + >
              All other characters are ignored (space, underscore ). They help formatting the dialstring.
            - Commands that use a numerical parameter, can be used without a numerical value. In this case the command will be issued with the value zero.
            - If the command string contains two consecutive commands without parameter, as discussed above, the modem will respond with an error.
            - After the command ATZ has been issued, a pause of two seconds should be respected before entering the next commands.
        """
        if not self.isPortCreated():
            return False
        self.__Port.write(atCommand + "\r\n")
        while True:
            linia = self.__Port.readline().strip()
            if linia != '':
                self.__Log.append(linia)
            if (linia == "OK" or linia =="ERROR"):
                break
#        print self.__Log                     # uncomment if you want to see whole log each time you send AT command
        if self.__Log[-1].endswith('OK'):
            print atCommand + " command: OK!\n"
            return True
        elif self.__Log[-1].endswith('ERROR'):
            print atCommand + " command: ERROR!\n"
            return False
        else:
            raise Exception, 'AT Command: ' + atCommand + ' not supported by phone!'

    def __initialize(self):
        """
        Initializes phone modem
        """
        if self.__Port.isOpen():
            self.__sendATCommand('ATZ')       # modem reset, should be 2 second pause here, but probably won't be necessary
            sleep(2)
            self.__sendATCommand('AT+CLIP=1') # identifying caller
            return True
        else: 
            return False

    def checkStatus(self):
        """
        Checks activity status of the mobile phone. Retuns an integer:
        
        Returned values:
            -1 - when didn't read the CPAS value or Port doesn't exist
             0 - Ready
             1 - Unavailable
             2 - Status unknown
             3 - Ringing
             4 - Call in progress
             5 - Asleep
        """
        if not self.isPortCreated():
            return -1        
        if self.__sendATCommand('AT+CPAS'):
            sleep(0.2)
            lines = self.__Log
            lines.reverse()
            currentStatus = "default"
            for line in lines:
                if line.startswith("+CPAS"):
                    currentStatus = line[-1]
#            print "current status:", currentStatus
            return int(currentStatus)
        else:
            return -1


##    Call methods - using AT commands

    def makeCall(self, phoneNumber):
        """
        Calls to phoneNumber
        """
        if not self.isPortCreated():
            return False
        if self.__sendATCommand('ATD' + phoneNumber + ";"):
            print "calling %s" % (phoneNumber)
            return True
        else:
            return False
        
    def acceptCall(self):
        """
        Accepts call
        
        NOTE: It can take a while before accept a call, especially when nobody is ringing and phone doesn't support AT+CPAS command
        """
        if not self.isPortCreated():
            return False
        currentStatus = self.checkStatus()
        if currentStatus == 3:                           # somebody is ringing
            return self.acceptCallNow()
        elif (currentStatus != 3 and currentStatus == -1): # if AT+CPAS command is not supported 
            if self.__Log[-1] == 'ERROR':
                print "accepting in progress..."
                for i in xrange(5):                      # there are few tries to check if anybody is calling
                    sleep(1)
#                    print "try [%s]" % i
                    self.__sendATCommand('AT+DPA')
                    if self.__Log[-3] == 'ERROR':
                        pass                             # nobody called... yet
                    else:
                        print "calling:", self.__Log[-3]
                        return self.acceptCallNow()      # somebody is calling
                return False                             # if tried 5 times and nobody called
        else:
            return False                                 # when cell status is not ready (and AT+CPAS is implemented) 
        
    def acceptCallNow(self):
        """
        Accepts call immediately, without checking if somebody is ringing
        
        NOTE: Using acceptCallNow() without checking if somebody is calling can hang up program!
        """
        if self.__sendATCommand('ATA'):
            print "accepted call!"
            return True
        else:
            print "call wasn't accepted!"
            return False

    def rejectCall(self):
        """
        Rejects call
        """
        if not self.isPortCreated():
            return False        
        if self.__sendATCommand('AT+CHUP'):
            print "rejected call"
            return True
        else:
            return False
        
    def checkCall(self):
        """
        Checks who is calling right now
        
        Returns a dictionary with keys:
            'Name' - caller name
            'Number' - caller phone number
        """
        if not self.isPortCreated():
            return False
        if self.checkStatus() != 3:
            print "nobody is calling!"
            return False
        if self.__sendATCommand('AT+CLIP=1'):
            lines = self.__Log
            lines.reverse()
            caller = {"Name": "", "Number": ""}
            for line in lines:
                if line.startswith("+CLIP:"):
                    response = line.split("\"")
                    caller["Number"] = response[1]
                    caller["Name"] = searchPhonebookForNumber(caller["Number"])
                    return caller
        else:
            return False
    
##    Call methods - using gammu - not always working, unused now

    def gammuMakeCall(self, phoneNumber):
        """
        Calls to phoneNumber via gammu
        """
        if not self.__State == None:
            return False
        self.__State.DialVoice(phoneNumber)
        return True

    def gammuAcceptCall(self):
        """
        Accepts call via gammu
        """
        if not self.__State == None:
            return False
        self.__State.AnswerCall(All=True)
        return True

    def gammuRejectCall(self):
        """
        Accepts call via gammu
        """
        if not self.__State == None:
            return False
        self.__State.CancelCall(All=True)
        return True

##    SMS methods - using gammu

    def sendSMS(self, text, phoneNumber):
        """
        Automagically sends SMS containing text to phoneNumber
        """
        if not self.isGammuSetUp():
            return False
        message = {'Text': text, 'SMSC': {'Location': 1}, 'Number': phoneNumber}
        self.__State.SendSMS(message)
        print "\nSent message \"%s\" to %s!\n" % (text, phoneNumber)
        return True
    
    def getAllSMS(self):
        """
        Gets all SMS from the phone.
        
        Returns list of dictionaries which keys are:
            'Location' - an unique ID of SMS in memory
            'Number' - sender/receiver phone number (depends on 'State')
            'Date' - date and time when SMS was sent
            'State' - can be: Sent, Read, UnSent, UnRead
            'Length' - SMS length in characters
            'Memory' - memory in which SMS is stored (often SM or ME)
        """
        if not self.isGammuSetUp():
            return
        self.__smsList = []    # list of dictionaries containing all SMS
        tmpSmsList = []        # temporary SMS list of dictionaries with all entries recieved from gammu
        smsStatus = self.__State.GetSMSStatus()
        remain = smsStatus['SIMUsed'] + smsStatus['PhoneUsed']
        start = True
        while remain > 0:
            try:
                if start:
                    currentSms = self.__State.GetNextSMS(Start = True, Folder = 0)
                    start = False
                else:
                    currentSms = self.__State.GetNextSMS(Location = currentSms[0]['Location'], Folder = 0)
                remain = remain - len(currentSms)
                tmpSmsList.append(currentSms)
            except gammu.ERR_EMPTY:
                pass
        
        linkedSmsList = gammu.LinkSMS(tmpSmsList)
        for i in linkedSmsList:
            decodedSms = gammu.DecodeSMS(i)
            locations = []
            for concatenated in i:
                locations.append(str(concatenated['Location']))
            Text = ""
            if decodedSms == None:
                Text = i[0]['Text']
            else:
                for entry in decodedSms['Entries']:
                    if entry['Buffer'] != None:
                        Text = entry['Buffer']
            self.__smsList.append({'Location': locations, 'Number': i[0]['Number'], 'DateTime': str(i[0]['DateTime']), 'State': i[0]['State'], 'Text': Text, 'Length': i[0]['Length'], 'Memory': i[0]['Memory']})
                                      
#        for i in self.__smsList:                                # uncomment if you want see SMS list written on console
#            print '%-15s: %s' % ('Location', i['Location'])
#            print '%-15s: %s' % ('Number', i['Number'])
#            print '%-15s: %s' % ('Date', str(i['DateTime']))
#            print '%-15s: %s' % ('State', i['State'])
#            print '%-15s: %s' % ('Length', i['Length'])
#            print '%-15s: %s' % ('Memory', i['Memory'])
#            print '%s' % i['Text']
#            print '\n',
    
        print "\nRead all SMS!"
        return self.__smsList
    
    def showSMSList(self):
        """
        Returns SMS list:
            - if list was previously downloaded from phone, returns self.__smsList
            - if self.__smsList doesn't exist, it calls getAllSMS() method
        """
        try:
            self.__smsList[0]
        except:
            print "No sms list was found!"
            return self.getAllSMS()
        else:
            return self.__smsList
        
    def getSMS(self, Location):
        """
        Gets a single SMS which 'Location' key matches the Location parameter
        
        Parameters:
            Location - an unique ID of SMS in memory
        """
        if not self.isGammuSetUp():
            return
        thisSMS = {}
        
#        if (self.__smsList != []):                                # uncomment if you want see SMS written on console
#            for i in self.__smsList:
#                if i['Location'] == Location:
#                    thisSMS = i
#                    print '%-15s: %s' % ('Location', i['Location'])
#                    print '%-15s: %s' % ('Number', i['Number'])
#                    print '%-15s: %s' % ('Date', str(i['DateTime']))
#                    print '%-15s: %s' % ('State', i['State'])
#                    print '%-15s: %s' % ('Length', i['Length'])
#                    print '%-15s: %s' % ('Memory', i['Memory'])
#                    print '%s' % i['Text']
#                    print '\n',
                    
        if thisSMS != {}:
            print "Read SMS: Location %i" % (Location)
        else:
            print "There isn't any SMS at Location %i" % (Location)
        return thisSMS
        
    def delAllSMS(self, Folder = -1):
        """
        Deletes all SMS from phone or SIM memory. 
        
        Parameters:
            Folder - can be 0 (default, in most phones it's what you want), 1, 2, 3, 4, 5 where:
            +---------------------------------+
            |   x   | Read  | Sent  | UnSent  |
            |---------------------------------|
            |   SM  |   0   |   1   |  4 (?)  |
            |---------------------------------|
            |   ME  |   2   |   3   |  5 (?)  |
            +---------------------------------+
        """
        if not self.isGammuSetUp():
            return False
        if Folder >= 0:
            self.__State.DeleteSMSFolder(Folder)
        print "Deleted all SMS!"
        return True
    
    def delSMS(self, Folder = 0, Location = 0):
        """
        Deletes a single SMS from a phone. 
        
        Parameters:
            Location - an unique ID of SMS in memory
            Folder - can be 0 (default, in most phones it's what you want), 1, 2, 3, 4, 5 where:
            +---------------------------------+
            |   x   | Read  | Sent  | UnSent  |
            |---------------------------------|
            |   SM  |   0   |   1   |  4 (?)  |
            |---------------------------------|
            |   ME  |   2   |   3   |  5 (?)  |
            +---------------------------------+    
        """
        if not self.isGammuSetUp():
            return False
        if type(Location) is list:
            for currentLocation in Location:
                self.__State.DeleteSMS(Folder, int(currentLocation))
                print "Deleted SMS: Location %i in Folder %i" % (int(currentLocation), Folder)
            return True
        elif type(Location) is str:
            correctList = list(Location.strip("[]").split(", "))
            correctLocation = []
            for item in correctList:
                correctLocation.append(item[1:-1])
            del correctList
            for currentLocation in correctLocation:
                self.__State.DeleteSMS(Folder, int(currentLocation))
                print "Deleted SMS: Location %i in Folder %i" % (int(currentLocation), Folder)
        else:
            try:
                self.__State.DeleteSMS(Folder, int(Location))
            except:
                return False
            return True

        
##    Phonebook methods - using gammu

    def getPhonebook(self):
        """
        Get contact list from the phone
        
        Returns list of dictionaries which keys are:
            'Location' - unique ID of each entry in phonebook
            'Memory' - memory in which entry is stored (often SM or ME)
            'Text' - contact name
            'Number' - contact number
        """
        if not self.isGammuSetUp():
            return
        self.__phonebookList = []
        tmpPhonebookList = []
        
        memory = ["ME", "SM"] # downloading entries from ME and SM
        for mem in memory:
            phonebookStatus = self.__State.GetMemoryStatus(Type = mem)
            remain = phonebookStatus['Used']
            location = 1
            while remain > 0:
                try:
                    currentEntry = self.__State.GetMemory(Type = mem, Location = location)
                    remain = remain - 1
                    tmpPhonebookList.append(currentEntry)
                except gammu.ERR_EMPTY:
                    pass
                location += 1
            
        for i in tmpPhonebookList:
            self.__phonebookList.append({'Location': i['Location'], 'Memory': i['MemoryType'], 'Text': i['Entries'][1]['Value'], 'Number': str(i['Entries'][0]['Value'])})
        
#        for i in self.__phonebookList:                        # uncomment if you want see phonebook written on console
#            print '%-15s: %s' % ('Location', i['Location'])
#            print '%-15s: %s' % ('Memory', i['Memory'])
#            print '%-15s: %s' % ('Text', i['Text'])
#            print '%-15s: %s' % ('Number', i['Number'])
#            print '\n',
        
        print "\nGot whole phonebook!"
        return self.__phonebookList
    
    def showPhonebookList(self):
        """
        Returns phonebook entries:
            - if list was previously downloaded from phone, returns self.__phonebookList
            - if self.__phonebookList doesn't exist, it calls getPhonebook() method
        """
        try:
            self.__phonebookList[0]
        except:
            print "No phonebook list was found!"
            return self.getPhonebook()
        else:
            return self.__phonebookList
        
    def searchPhonebookForNumber(self, Number):
        """
        Searches local copy of phonebook entries (if exists) for a Number
        
        Returns: Contact name (or False if haven't found any)
        """
        if not self.isGammuSetUp():
            return False
        try:
            self.__phonebookList[0]
        except:
            print "No phonebook list was found!"
            return False
        else:
            for entry in self.__phonebookList:
                if entry["Number"].find(Number) != -1:
                    return entry["Text"]
        return False
        
    def searchPhonebookForLocation(self, Location = -1):
        """
        Searches local copy of phonebook entries (if exists) for Location
        
        Returns: Found entry (or False if haven't found any)
        """
        if not self.isGammuSetUp():
            return False
        try:
            self.__phonebookList[0]
        except:
            print "No phonebook list was found!"
            return False
        else:
            for entry in self.__phonebookList:
                if entry["Location"] == Location:
                    return entry
        return False
    
    def addToPhonebook(self, Number, Name, Memory):
        """
        Adds Record to phonebook
        
        Parameters:
            'Number' - contact number
            'Name' - contact name
            'Memory' - memory in which entry is stored (often SM or ME)
        """
        if not self.isGammuSetUp():
            return False
        
        if type(Number) is not unicode:
            Number_General = unicode(Number, "iso-8859-2")
        else:
            Text_Name = Number
        if type(Name) is not unicode:
            Text_Name = unicode(Name, "iso-8859-2")
        else:
            Text_Name = Name
        Record = {
                  'Entries': [{'Type': 'Number_General','Value': Number_General},{'Type': 'Text_Name','Value': Text_Name}],
                  'MemoryType': Memory
                  }
        Location = self.__State.AddMemory(Record)
        print "Added phonebook entry %s at location %i" % (Record, Location)
        return True
        
    def delFromPhonebook(self, Memory, Location):
        """
        Deletes entry from Memory
        
        Parameters:
            'Memory' - memory in which entry is stored (often SM or ME)
            'Location' - unique ID of entry in phonebook
        """
        if not self.isGammuSetUp():
            return False
        if type(Location) is int:
            pass
        elif type(Location) is str:
            Location = int(Location)
        else:
            Location = int(Location)
        self.__State.DeleteMemory(Location, Memory)
        print "Deleted phonebook entry %s from location %s" % (Memory, str(Location))
        return True
            
if __name__ == "__main__":
    print __doc__
    